import io
import json
import mimetypes
import uuid
from typing import Any, Dict, Iterable, Optional, Sequence, Tuple
from urllib import error as urllib_error
from urllib import request as urllib_request
from urllib.parse import urlencode


class DifyClientError(RuntimeError):
    """Raised when the Dify API responded with an error."""


class DifyClient:
    API_URL = "https://api.dify.ai/v1/chat-messages"
    MESSAGES_URL = "https://api.dify.ai/v1/messages"
    CONVERSATIONS_URL = "https://api.dify.ai/v1/conversations"
    FILES_UPLOAD_URL = "https://api.dify.ai/v1/files/upload"

    def __init__(self, api_key: str, response_mode: str = "blocking") -> None:
        self.api_key = api_key
        self.response_mode = response_mode

    def _build_payload(
        self,
        *,
        query: str,
        user_id: str,
        conversation_id: str = "",
        inputs: Optional[Dict[str, Any]] = None,
        files: Optional[Iterable[Dict[str, Any]]] = None,
    ) -> Dict[str, Any]:
        payload: Dict[str, Any] = {
            "query": query,
            "user": user_id,
            "conversation_id": conversation_id,
            "inputs": inputs or {},
            "response_mode": self.response_mode,
        }
        if files:
            payload["files"] = list(files)
        return payload

    def _encode_multipart_formdata(
        self,
        fields: Sequence[Tuple[str, str]],
        files: Sequence[Tuple[str, str, str, bytes]],
    ) -> Tuple[bytes, str]:
        boundary = uuid.uuid4().hex
        body = io.BytesIO()
        boundary_bytes = boundary.encode()

        for name, value in fields:
            body.write(b"--" + boundary_bytes + b"\r\n")
            body.write(
                f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode()
            )
            body.write(value.encode())
            body.write(b"\r\n")

        for field_name, filename, content_type, data in files:
            body.write(b"--" + boundary_bytes + b"\r\n")
            disposition = (
                f'Content-Disposition: form-data; name="{field_name}"; filename="{filename}"\r\n'
            )
            body.write(disposition.encode())
            body.write(f"Content-Type: {content_type}\r\n\r\n".encode())
            body.write(data)
            body.write(b"\r\n")

        body.write(b"--" + boundary_bytes + b"--\r\n")
        content_type_header = f"multipart/form-data; boundary={boundary}"
        return body.getvalue(), content_type_header

    def upload_file(
        self,
        *,
        user_id: str,
        file_name: str,
        file_data: bytes,
        content_type: Optional[str] = None,
        field_name: str = "file",
    ) -> Dict[str, Any]:
        if not file_data:
            raise ValueError("file_data must not be empty")

        guessed_type = content_type or mimetypes.guess_type(file_name)[0]
        normalized_type = guessed_type or "application/octet-stream"

        fields: list[Tuple[str, str]] = [("user", user_id)]
        files: list[Tuple[str, str, str, bytes]] = [
            (field_name, file_name, normalized_type, file_data)
        ]
        body, content_type_header = self._encode_multipart_formdata(fields, files)

        request = urllib_request.Request(
            self.FILES_UPLOAD_URL,
            data=body,
            method="POST",
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": content_type_header,
                "Accept": "application/json",
            },
        )
        return self._execute(request)

    def ask(
        self,
        *,
        query: str,
        user_id: str,
        conversation_id: str = "",
        inputs: Optional[Dict[str, Any]] = None,
        files: Optional[Iterable[Dict[str, Any]]] = None,
    ) -> Dict[str, Any]:
        payload = self._build_payload(
            query=query,
            user_id=user_id,
            conversation_id=conversation_id,
            inputs=inputs,
            files=files,
        )
        request_data = json.dumps(payload).encode("utf-8")
        request = urllib_request.Request(
            self.API_URL,
            data=request_data,
            method="POST",
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
            },
        )
        return self._execute(request)

    def get_messages(
        self,
        *,
        user_id: str,
        conversation_id: str,
        limit: int = 20,
        last_id: str = "",
    ) -> Dict[str, Any]:
        if not conversation_id:
            return {"data": [], "has_more": False, "limit": limit}

        params = {
            "user": user_id,
            "conversation_id": conversation_id,
            "limit": limit,
        }
        if last_id:
            params["last_id"] = last_id

        query_string = urlencode(params)
        request = urllib_request.Request(
            f"{self.MESSAGES_URL}?{query_string}",
            method="GET",
            headers={
                "Authorization": f"Bearer {self.api_key}",
            },
        )
        return self._execute(request)

    def list_conversations(
        self,
        *,
        user_id: str,
        limit: int = 20,
        last_id: str = "",
    ) -> Dict[str, Any]:
        params = {
            "user": user_id,
            "limit": limit,
        }
        if last_id:
            params["last_id"] = last_id

        query_string = urlencode(params)
        request = urllib_request.Request(
            f"{self.CONVERSATIONS_URL}?{query_string}",
            method="GET",
            headers={
                "Authorization": f"Bearer {self.api_key}",
            },
        )
        return self._execute(request)

    def delete_conversation(self, *, user_id: str, conversation_id: str) -> Dict[str, Any]:
        request_data = json.dumps({"user": user_id}).encode("utf-8")
        request = urllib_request.Request(
            f"{self.CONVERSATIONS_URL}/{conversation_id}",
            data=request_data,
            method="DELETE",
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
                "Accept": "application/json",
            },
        )
        return self._execute(request)

    def _execute(self, request: urllib_request.Request) -> Dict[str, Any]:
        try:
            with urllib_request.urlopen(request) as response:
                body = response.read().decode("utf-8")
                if response.status >= 400:
                    raise DifyClientError(
                        f"Dify API error {response.status}: {body}"
                    )
                # 处理 204 No Content 响应
                if response.status == 204 or not body:
                    return {}
                return json.loads(body)
        except urllib_error.HTTPError as exc:
            try:
                error_body = exc.read().decode("utf-8")
            except Exception:  # pragma: no cover - best effort diagnostics
                error_body = "<no body>"
            raise DifyClientError(
                f"Dify API error {exc.status}: {error_body}"
            ) from exc
        except urllib_error.URLError as exc:
            raise DifyClientError(f"Dify API unreachable: {exc.reason}") from exc
        except json.JSONDecodeError as exc:
            raise DifyClientError(f"Invalid JSON response from Dify API: {exc}") from exc
